﻿using System;
using System.Linq;

namespace MLSharp.Clustering
{
	/// <summary>
	/// Generates synthetic data sets that consist of only continuous attributes.
	/// </summary>
	public class ContinuousKfdDataSetGenerator : KnowledgeFrontierDataSetGeneratorBase
	{
		#region Logger

		/// <summary>
		/// The logger for the type.
		/// </summary>
		private static readonly log4net.ILog mLogger =
			log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);

		#endregion

		#region Private Fields

		/// <summary>
		/// Stores the index of the accuracy attribute.
		/// </summary>
		private int mAccuracyIndex;

		/// <summary>
		/// Stores the ID of the frontier ID attribute.
		/// </summary>
		private int mFrontierIdIndex;

		/// <summary>
		/// The sequential ID of the next frontier node.  
		/// </summary>
		private int mFrontierId;

		#endregion

		#region Public Properties

		#endregion

		#region Private Methods

		/// <summary>
		/// Recursively populates a dataset with instances that naturally form clusters.
		/// </summary>
		/// <param name="dataSet"></param>
		/// <param name="level"></param>
		/// <param name="instanceStartIndex"></param>
		/// <param name="instanceEndIndex"></param>
		/// <param name="attributeStartIndex"></param>
		/// <param name="frontierProbability"></param>
		private void PopulateInstances(IDataSet dataSet, int level, int instanceStartIndex, int instanceEndIndex, int attributeStartIndex, double frontierProbability)
		{
			if (mLogger.IsDebugEnabled)
			{
				mLogger.DebugFormat(
					"Generating instances for level {0}, instances {1} through {2}, attributes starting at {3}, frontier probability is {4}",
					level, instanceStartIndex, instanceEndIndex, attributeStartIndex, frontierProbability);
			}

			//Populate instances
			//This is the number of attributes for the current level.
			int numAttributes = level * AttributeFactor;

			//Populate the current range of attributes
			PopulateInstances(dataSet, instanceStartIndex, instanceEndIndex, attributeStartIndex, numAttributes);

			//Check for knowledge frontier creation
			if (frontierProbability > 0)
			{
				//If this is the last level or if random chance says its time
				//to create a frontier
				if (level == 1 || frontierProbability >= mRand.NextDouble())
				{
					MarkKnowledgeFrontier(dataSet, instanceStartIndex, instanceEndIndex);

					//Since we've created a frontier, the probability of creating a frontier further down the tree
					//is now zero.
					frontierProbability = 0.0;
				}
				//Otherwise, increase the probability of creating a frontier next time.
				else
				{
					frontierProbability += (1.0 - FrontierProbability) / Depth;

					if (mLogger.IsDebugEnabled)
					{
						mLogger.DebugFormat("New frontier probability: {0}.", frontierProbability);
					}
				}
			}

			//If this isn't the last level, recurse.
			if (level > 1)
			{
				//Determine how to split things up.  We need one fewer split points
				//than the desired number of branches.
				int[] splitPoints = new int[BranchingFactor - 1];

				for (int i = 0; i < splitPoints.Length; i++)
				{
					splitPoints[i] = instanceStartIndex + (instanceEndIndex - instanceStartIndex)*(i + 1)/BranchingFactor;
				}

				//Recurse
				for (int i = 0; i <= splitPoints.Length; i++)
				{
					int startIndex = i > 0 ? splitPoints[i - 1] + 1 : instanceStartIndex;

					int endIndex = i < splitPoints.Length ? splitPoints[i] : instanceEndIndex;

					PopulateInstances(dataSet, level - 1, startIndex, endIndex, attributeStartIndex + numAttributes, frontierProbability);
				}
			}
		}

		/// <summary>
		/// Marks the current range of instances a knowledge frontier by seeding them with
		/// similar accuracy values and (optionally) labeling them.
		/// </summary>
		/// <param name="dataSet"></param>
		/// <param name="instanceStartIndex"></param>
		/// <param name="instanceEndIndex"></param>
		private void MarkKnowledgeFrontier(IDataSet dataSet, int instanceStartIndex, int instanceEndIndex)
		{
			//This will be used as a seed to populate the accuracy values for all instances at this level.
			double accuracySeed = mRand.NextDouble();

			if (mLogger.IsDebugEnabled)
			{
				mLogger.DebugFormat("Creating knowledge frontier, seed accuracy is {0}.", accuracySeed);
			}

			for (int i = instanceStartIndex; i <= instanceEndIndex; i++)
			{
				//Apply a little bit of noise to the accuracy seed.
				double noise = AccuracyNoise * mRand.NextDouble() * (mRand.NextDouble() > 0.5 ? -1 : 1);

				double accuracy = accuracySeed + noise;
				//Accuracy must be between 0 and 100.
				accuracy = Math.Max(0.0, accuracy);
				accuracy = Math.Min(100.0, accuracy);
				dataSet.Instances[i].Values[mAccuracyIndex] = accuracy;

				//If we're supposed to label items with their frontier, go ahead and apply the label.
				if (LabelFrontiers)
				{
					dataSet.Instances[i].Values[mFrontierIdIndex] = mFrontierId;
				}
			}

			//If we're labeling frontiers, go ahead and increment to the next ID.
			if (LabelFrontiers)
			{
				mFrontierId++;
			}

			return;
		}


		#endregion

		#region Public Methods

		/// <summary>
		/// Generates a synthetic dataset suitable for hierarchical clustering.
		/// </summary>
		/// <returns>The dataset.</returns>
		public override IDataSet GenerateDataSet()
		{
			if (mLogger.IsDebugEnabled)
			{
				mLogger.DebugFormat("Generating dataset using AttributeFactor={0}, NoiseFactor={1}", AttributeFactor, NoiseFactor);
			}

			IDataSet dataSet = BuildDataSet();

			dataSet.Attributes.Add(DataAttribute.NewContinuousAttribute("Accuracy"));

			//Grab the index of the accuracy attribute, we'll need it later.
			mAccuracyIndex = dataSet.Attributes.Count - 1;

			if (LabelFrontiers)
			{
				dataSet.Attributes.Add(DataAttribute.NewContinuousAttribute("FrontierId"));

				//Grab this index, too.
				mFrontierIdIndex = dataSet.Attributes.Count - 1;

				//Start numbering frontiers at 1.
				mFrontierId = 1;
			}

			//Populate the dataset with empty instances.
			for (int i=0; i < NumberOfInstances; i++)
			{
				dataSet.Instances.Add(new Instance(Enumerable.Repeat(double.NaN, dataSet.Attributes.Count).ToArray()));
			}

			//Starting probability of creating a frontier.
			double frontierProbability = FrontierProbability;

			//Recursively populate the dataset.
			PopulateInstances(dataSet, Depth, 0, dataSet.Instances.Count - 1, 0, frontierProbability);

			return dataSet;
		}

		#endregion
	}
}
